﻿using Bettzueche.RLLibrary.Util;
using Bettzueche.RLLibrary.Types;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace Bettzueche.Dampfross
{
    /// <summary>
    /// TODO: Agent-Beschreibung
    /// </summary>
    public class Agent
    {
        Karte _karte;
        /// <summary>The _qe matrix (Aktion x Zustand)</summary>
        WETupel[][] _qeMatrix;
        EpsilonGreedy _policy;
        TraceSet<SATupel> _pfad;

        /// <summary>
        /// Sets the umwelt. Sollte nur zu Beginn einer Lernepisode und nicht währenddessen gesetzt werden.
        /// </summary>
        /// <value>
        /// The umwelt. Sollte nicht <c>null</c> sein.
        /// </value>
        public Karte Umwelt {
            internal get { return _karte; }
            set {
                _karte = value;
            }
        }

        public double RewardTotal { get; private set; }

        /// <summary>
        /// Gets or sets die Lernrate α. Standard = 0.5.
        /// </summary>
        /// <value>
        /// The α von 0.0 bis 1.0.
        /// </value>
        public double Alpha { get; set; }

        /// <summary>
        /// Gets or sets Abschwächungsfaktor γ. Standard = 0.95.
        /// </summary>
        /// <value>
        /// The γ von 0.0 bis 1.0.
        /// </value>
        public double Gamma { get; set; }

        /// <summary>
        /// Gets or sets the Spurzerfallsfaktor λ. Standard = 0.9.
        /// </summary>
        /// <value>
        /// The λ von 0.0 bis 1.0.
        /// </value>
        public double Lambda { get; set; }

        /// <summary>
        /// Gets or sets minimaler E-Wert, der nicht zu 0 interpretiert wird.
        /// Standard = 0.9^50 = ca. 0,5 %
        /// </summary>
        /// <value>
        /// The minimum berechtigung.
        /// </value>
        public double MinBerechtigung { get; set; }

        /// <summary>
        /// Gets or sets Die Explorationsrate ε. Standard = 0.1.
        /// </summary>
        /// <value>
        /// The ε von 0.0 bis 1.0.
        /// </value>
        public double Epsilon {
            get { return _epsilon; }
            set { 
                _epsilon = value;
                _policy = new EpsilonGreedy(_epsilon);
            }
        }
        double _epsilon;

        /// <summary>
        /// Initializes a new instance of the <see cref="Agent"/> class.
        /// </summary>
        /// <param name="karte">Referenz auf die "Umwelt.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        public Agent(Karte karte) {
            if (karte == null) {
                throw new ArgumentNullException();
            }
            _karte = karte;
            InitKnowledgeBase();
            Alpha = 0.5;
            Gamma = 0.95;
            Lambda = 0.9;
            MinBerechtigung = Math.Pow(Lambda, 50);
            Epsilon = 0.1;
            _pfad = new TraceSet<SATupel>();
        }


        /// <summary>
        /// TODO: DoStep-Beschreibung.
        /// </summary>
        /// <param name="aktion">The aktion.</param>
        /// <returns></returns>
        public HexRichtungen DoStep(HexRichtungen aktion) {
            Umgebung state = _karte.AktuellerZustand;
            Umgebung newState;
            double reward;
            _karte.MacheSpielzug(aktion, out reward, out newState);
            RewardTotal += reward;

            int sIdx = GibZustandsIndex(state);
            int nsIdx = GibZustandsIndex(newState);
            HexRichtungen nextAction = _policy.GetAction(GibQWerte(nsIdx));
            double delta = reward + Gamma * _qeMatrix[(int)nextAction][nsIdx].W - _qeMatrix[(int)aktion][sIdx].W;

            double decayRate = Gamma * Lambda; // γ·λ
            double qUpdateFactor = Alpha * delta; // α·δ
            
            _pfad.Add(new SATupel(sIdx, aktion));  // add to visited (s,a)'s
            var removeFromPath = new HashSet<SATupel>();
            // E = γ·λ·E; e_sx = 0; e_sa = 1;
            // W = W + α·δ·E
            foreach (var tupel in _pfad) {
                int s = tupel.S;
                HexRichtungen a = tupel.A;
                double e_i;
                WETupel qe_sa = _qeMatrix[(int)a][s];
                if (s.Equals(sIdx)) {
                    if (a == aktion) {
                        e_i = 1.0;
                    }
                    else {
                        e_i = 0.0;
                        removeFromPath.Add(tupel);
                    }
                }
                else {
                    e_i = qe_sa.E * decayRate;
                    if (e_i < MinBerechtigung) { // üblicherweise ein oder kein Element aus Path
                        e_i = 0.0;
                        removeFromPath.Add(tupel);
                    }
                }
                qe_sa.E = e_i;
                qe_sa.W += qUpdateFactor * qe_sa.E;
                _qeMatrix[(int)a][s] = qe_sa;  // WETupel jetzt struct (ValueType)!!!
            }
            foreach (var tupel in removeFromPath) {
                _pfad.Remove(tupel);
            }
            //----------------------
            //DebugPrint(aktion);
            //------------------------
            return nextAction;
        }

        /// <summary>
        /// Gibt die mittlere quaratischer Abweichung der Q-Werte zu den Test-Zuständen.
        /// <see cref="Agent#SetStates(int[])"/> - Die betroffenen Zustand-IDs müssen gesetzt sein.
        /// <see cref="Agent#SetSollwerte(int[][])"/> - Die zugehörigen Sollwerte müssen gesetzt sein. In derselben Zustands-Reihenfolge
        /// wie im SetStates-Array und mit der durch HexRichtungen vorgegebenen Reihenfolge der Aktionen.
        /// </summary>
        /// <returns></returns>
        internal double GetMSE() {
            //HexKoordinate actualPosition = _karte._aktuellePosition;
            int stateCnt = 0;
            double sqDiffs = 0.0;
            foreach (int s in _debugStates) {
                for (int a = 0; a < 6; a++) {
                    WETupel we = _qeMatrix[a][s];
                    double soll = -_debugSollwerte[stateCnt][a];
                    double diff = we.W - soll; // soll ist positiv, W aber negativ angegeben!
                    sqDiffs += diff * diff;
                }
                stateCnt++;
            }
            sqDiffs = sqDiffs / (stateCnt * 6);
            return sqDiffs;
        }

        /// <summary>
        /// Für den 2-Welten-Test. Gib alle Q(s,a)-Werte, bzw. die der den Welten gemeinsamen Zustände.
        /// Voraussetzung dafür ist, dass mit SetStates nur die gemeinsamen Zustände übergeben wurden.
        /// </summary>
        /// <returns>Q-Werte der Debug-Zustände (s x a)</returns>
        /// <remarks>
        /// Gibt je Zustand und je Aktion (6 Aktionen=HexRichtungen je Zustand) einen Double-Wert
        /// --&gt; eine #S x 6 Dbl-Matrix
        /// </remarks>
        internal Matrix GetQVals() {
            int actionCnt = Enum.GetValues(typeof(HexRichtungen)).Length;
            Matrix qVals = new Matrix(_debugStates.Length, actionCnt);
            int sIdx = 0;
            foreach (int s in _debugStates) {
                for (int a = 0; a < actionCnt; a++) {
                    qVals[sIdx,a] = _qeMatrix[a][s].W;
                }
                sIdx++;
            }
            return qVals;
        }



        static readonly ConsoleColor badRed = ConsoleColor.Red;
        static readonly ConsoleColor nearYellow = ConsoleColor.Yellow;
        static readonly ConsoleColor goodGreen = ConsoleColor.Green;
        static readonly ConsoleColor standardWhite = ConsoleColor.White;

        //[Conditional("DEBUG")]
        double DebugPrint(HexRichtungen lastAction) {
            HexKoordinate actualPosition = _karte._aktuellePosition;
            Console.CursorLeft = 0;
            Console.CursorTop = 0;
            Console.WriteLine("letzte Aktion: {0,2} / akt. Position: {1,-11}", lastAction, actualPosition);
            int stateCnt = 0;
            double sqDiffs = 0.0;
            foreach (int s in _debugStates) {
                Console.ForegroundColor = Agent.standardWhite;
                Console.WriteLine("s#{0:N0}", s);
                Console.Write("\t");
                for (int a = 0; a < 6; a++) {
                    WETupel we = _qeMatrix[a][s];
                    Console.ForegroundColor = Agent.standardWhite;
                    Console.Write("{0}:(", (HexRichtungen)a);
                    double soll = _debugSollwerte[stateCnt][a];
                    double diff = Math.Abs(-soll - we.W); // soll ist positiv, W negativ angegeben!
                    sqDiffs += (-soll - we.W) * (-soll - we.W);
                    if (diff <= (0.01 * soll))
                        Console.ForegroundColor = Agent.standardWhite;
                    else if (diff <= (0.1 * soll))
                        Console.ForegroundColor = Agent.goodGreen;
                    else if (diff <= (0.2 * soll))
                        Console.ForegroundColor = Agent.nearYellow;
                    else
                        Console.ForegroundColor = Agent.badRed;
                    Console.Write("{0,7:N3}", we.W);
                    Console.ForegroundColor = Agent.standardWhite;
                    Console.Write(" | {0:N3}) ", we.E);
                    if (a == 2) {
                        Console.WriteLine();
                        Console.Write("\t");
                    }
                }
                Console.WriteLine();
                stateCnt++;
            }
            sqDiffs /= stateCnt * 6;
            Console.WriteLine("MSE: {0:N3}", sqDiffs);
            //System.Threading.Thread.Sleep(300);
            //Console.Write("Press any key to continue... ");
            //Console.ReadKey(true);
            return sqDiffs;
        }

        /// <summary>
        /// Nur zu Testzwecken / für die Experimente!<para>
        /// Setze die Zustandsnummern, die ausgewertet werden sollen. Normalerweise alle z-IDs, die zur
        /// jeweiligen Umwelt gehören und in der Datei zIDsNxM-nn.txt gespeichert sind.
        /// </para><para>
        /// Für diese zustände wird in <c>GetMSE</c> der mittlere Error ermittelt.
        /// </para>
        /// </summary>
        /// <param name="states">The states.</param>
        [Conditional("DEBUG")]
        internal void SetStates(int[] states) {
            _debugStates = states;
        }
        int[] _debugStates;


        /// <summary>
        /// Nur zu Testzwecken / für die Experimente!<para>
        /// Setze die Sollwerte, die ausgewertet werden sollen. Normalerweise sortiert nach Zick-Zack-Reihendurchlauf
        /// durch die Karte und gespeichert in SollwerteNxM.csv.
        /// </para><para>
        /// Diese Sollwerte werden in <c>GetMSE</c> herangezogen.
        /// </para>
        /// </summary>
        /// <param name="sollwerte">Eine S x A Matrix der Q(s,a)-Sollwerte. Die Zustände S sollten dabei dieselbe Reihenfolge, wie im Array für SetStates haben
        /// und die Aktionen die Reihenfolge wie im Enum HexRichtungen (von NO guzs)</param>
        [Conditional("DEBUG")]
        internal void SetSollwerte(double[][] sollwerte) {
            _debugSollwerte = sollwerte;
        }
        double[][] _debugSollwerte;

        /// <summary>
        /// Runs the episode.
        /// </summary>
        /// <param name="maxActionCount">The maximum action count.</param>
        /// <returns>Anzahl ausgeführter Schritte</returns>
        public int RunEpisode(int maxActionCount) {
            // alles Initialisiert?
            int sIdx = GibZustandsIndex(_karte.AktuellerZustand);
            HexRichtungen action = _policy.GetAction(GibQWerte(sIdx)); // Q-Werte von Zustand #sIdx
            int stepCount = 0;
            while ((!_karte.IsFinal) && stepCount < maxActionCount) {
                action = this.DoStep(action);
                stepCount++;
            }
            return stepCount;
        }

        public void RunDebugEpisode(int maxActionCount, out int stepCount, out double reward, out double mse) {
            stepCount = RunEpisode(maxActionCount);
            // ---------------------------------------------
            mse = DebugPrint(HexRichtungen.NO);
            // --------------------------------------------
            reward = RewardTotal;
        }

        /// <summary>
        /// Agent (bei Episodenende) zurücksetzen.<para>
        /// RewardTotal = 0. Gelerntes bleibt erhalten! Properties auch.</para>
        /// </summary>
        public void Reset() {
            foreach (var tupel in _pfad) {
                WETupel we = _qeMatrix[(int)tupel.A][tupel.S];
                _qeMatrix[(int)tupel.A][tupel.S] = new WETupel(we.W, 0.0);
            }
            _pfad.Clear();
            RewardTotal = 0;
        }

        /// <summary>
        /// Amnesie! Setzt den Agent vollständig zurück und löscht dabei auch das Gelernte!
        /// Properties bleiben erhalten.
        /// </summary>
        public void Clear() {
            InitKnowledgeBase();
            _pfad.Clear();
            RewardTotal = 0;
        }

        /// <summary>
        /// Safes the experiance.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <exception cref="System.Runtime.Serialization.SerializationException">
        /// An error has occurred during serialization, such as if an object in the graph
        ///    parameter is not marked as serializable.
        /// </exception>
        /// <exception cref="System.Security.SecurityException">
        /// The caller does not have the required permission.
        /// </exception>
        public void SafeExperiance(string filename) {
            Stream stream = File.Open(filename, FileMode.Create);
            BinaryFormatter bFormatter = new BinaryFormatter();
            double[][] qWerte = new double[_qeMatrix[0].Length][];
            for (int i = 0; i < qWerte.Length; i++) {
                qWerte[i] = new double[_qeMatrix[1].Length];
            }
            for (int a = 0; a < _qeMatrix.GetLength(0); a++) {
                for (int s = 0; s < _qeMatrix.GetLength(1); s++) {
                    qWerte[a][s] = _qeMatrix[a][s].W;
                }
            }
            try {
                bFormatter.Serialize(stream, qWerte);
            }
            catch (Exception) {
                throw;
            }
            finally {
                stream.Close();
            }
        }

        /// <summary>
        /// Loads the experiance.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <exception cref="System.Runtime.Serialization.SerializationException">
        /// The serializationStream supports seeking, but its length is 0
        /// </exception>
        /// <exception cref="System.Security.SecurityException">
        /// The caller does not have the required permission.
        /// </exception>
        public void LoadExperiance(string filename) {
            double[][] qWerte;
            Stream stream = File.Open(filename, FileMode.Open);
            BinaryFormatter bFormatter = new BinaryFormatter();
            try {
                qWerte = (double[][])bFormatter.Deserialize(stream);
            }
            catch (Exception) {
                throw;
            }
            finally {
                stream.Close();
            }
            _qeMatrix = new WETupel[qWerte.GetLength(0)][];
            for (int i = 0; i < _qeMatrix.Length; i++) {
                _qeMatrix[i] = new WETupel[qWerte.GetLength(1)];
            }
            for (int a = 0; a < qWerte.GetLength(0); a++) {
                for (int s = 0; s < qWerte.GetLength(1); s++) {
                    _qeMatrix[a][s] = new WETupel(qWerte[a][s], 0.0);
                }
            }
        }

        /// <summary>
        /// Gibs the q werte.
        /// </summary>
        /// <param name="zustand">Index des Zustands.</param>
        /// <returns></returns>
        private double[] GibQWerte(int zustand) {
            double[] werte = new double[_aktionen.Length];
            foreach (HexRichtungen zug in _aktionen) {
                int a = (int)zug;
                werte[a] = _qeMatrix[a][zustand].W;
            }
            return werte;
        }

        /// <summary>
        /// Gibt ID des zustands.
        /// Durchnummerieren aller Zustandsausprägungen (insg. 120*9^6 = 63.772.920).
        /// </summary>
        /// <param name="zustand">The zustand.</param>
        /// <returns>ID des zustands</returns>
        internal static int GibZustandsIndex(Umgebung zustand) {
            /*
             * - 10 Entfernungen zum Ziel { <=1, 2, 3, 4, 5, 6-7, 8-11, 12-19, 20-35, >35}
             * - 6 Richtungen zum Ziel
             * - Zentrum ist Ebene(Stadt) oder Berg (Wasser theoretisch nicht möglich, weil unpassierbar)
             * - Umgebungsfeld ist Wasser, Ebene oder Berg. Ebene oder Berg hat (k)eine Flusskannte zum Zentrum
             *   und/oder zum linken Nachbarn (im Uzg Sinn); Stadt wird wie Ebene behandelt; => 9 Varianten für
             *   alle 6 Umgebungsfelder.
             */
            int idx;
            //Entfernung
            int i = Agent.Entfernungen.Length - 1; // 9
            for (; i >= 0; i--) {
                if (zustand.Entfernung >= Agent.Entfernungen[i])
                    break; // Entfernung gehört ins i'te Intervall
            }
            idx = i * _12mal9hoch6; // * 6.377.292 für die anderen Parameter

            //Richtung
            i = (int)zustand.RichtungZiel;
            idx += i *_2mal9hoch6; // 1062882

            // Zentrumsfeld
            Kachel feld = zustand.ZentrumFeld;
            if (feld.Content.Typ == Kacheltyp.Berg) {
                i = 1;
            }
            else { // Ebene oder Stadt gleichbehandelt; Wasser dürfte nicht vorkommen
                i = 0;
            }
            idx += i * PowerOf9[6];// 9^6

            // Felder NO bis SO
            int num = Agent.Flags.Length - 1; // =5
            foreach (Kachel kachel in zustand.Nachbarn) {
                feld = kachel;
                if (feld.Content.Typ == Kacheltyp.Wasser) {
                    i = 2;
                }
                else if (feld.Content.Typ == Kacheltyp.Berg) {
                    i = 1;
                }
                else { // Ebene oder Stadt
                    i = 0;
                }
                int j;
                // 4 Flussvarianten
                if (i == 2) { // für Wasser interessieren keine Flüsse
                    j = 0;
                }
                else {
                    Rivers flusse = feld.Content.FlussFlags & (Agent.Flags[num].Item1 | Agent.Flags[num].Item2);
                    if (flusse == Flags[num].Item1) {
                        j=1; // Fluss zum Zentrum
                    }
                    else if (flusse == Flags[num].Item2) {
                        j=2; // Fluss zum linken Nachbarn
                    }
                    else if (flusse == (Flags[num].Item1 | Flags[num].Item2)) {
                        j = 3; // Fluss zum Zentrum und linken Nachbarn
                    }
                    else {
                        j= 0; // ohne Fluss
                    }
                }
                idx += (i*4 + j) * PowerOf9[num];

                num--;
            }

            return idx;
        }

        /// <summary>
        /// Initialisiert die Q-Funktion (_qeMatrix) mit 0.
        /// </summary>
        private void InitKnowledgeBase() {
            _qeMatrix = new WETupel[6][];
            for (int i = 0; i < _qeMatrix.Length; i++)
                _qeMatrix[i] = new WETupel[_120mal9hoch6];
        }


        /// <summary>
        /// Flags für die Flussprüfung bei der Indizierung. Umgekehrt sortiert von SO-Feld im GUzg bis NO-Feld.<para>
        /// Hinweis zur Modellierung: Jedes Nachbarfeld hat an der Kante zum Zentrum und an der zum linken Nachbarn im UzgSinn
        /// ev. einen Flussübergang. Dadurch können alle möglichen Flüsse im Sichtfeld angegeben werden.</para>
        /// </summary>
        static readonly Tuple<Rivers, Rivers>[] Flags;
        static readonly int[] PowerOf9;
        static readonly int _2mal9hoch6;
        static readonly int _12mal9hoch6;
        static readonly int _120mal9hoch6;
        /// <summary>{ 0-1, 2, 3, 4, 5, 6-7, 8-11, 12-19, 20-35, >35 }. Array hält untere Intervallgrenzen.</summary>
        static readonly int[] Entfernungen = { 0, 2, 3, 4, 5, 6, 8, 12, 20, 36 };
        static readonly HexRichtungen[] _aktionen = (HexRichtungen[])Enum.GetValues(typeof(HexRichtungen)); //.Cast<HexRichtungen>().ToArray();

        static Agent() {
            Flags = new Tuple<Rivers, Rivers>[6]; // 6 Nachbarfelder
            Flags[5] = new Tuple<Rivers, Rivers>(Rivers.SudWest, Rivers.NordWest); // NO
            Flags[4] = new Tuple<Rivers, Rivers>(Rivers.Sud, Rivers.SudWest); // N
            Flags[3] = new Tuple<Rivers, Rivers>(Rivers.SudOst, Rivers.Sud); // NW
            Flags[2] = new Tuple<Rivers, Rivers>(Rivers.NordOst, Rivers.SudOst); // SW
            Flags[1] = new Tuple<Rivers, Rivers>(Rivers.Nord, Rivers.NordOst); // S
            Flags[0] = new Tuple<Rivers, Rivers>(Rivers.NordWest, Rivers.Nord); // SO
            PowerOf9 = new int[7];
            for (int i = 0; i < PowerOf9.Length; i++) {
                PowerOf9[i] = (int)Math.Pow(9, i);
            }
            _2mal9hoch6 = 2 * PowerOf9[6];
            _12mal9hoch6 = 6 * _2mal9hoch6;
            _120mal9hoch6 = 10 * _12mal9hoch6;
        }


        
    }


}
